/*=====================================================================

  File:      CommandLineParser.cs
  Summary:   SQL Remote Blob Storage provider installation utility.
             Utility class for parsing command line input.
  Date:	     June 24th, 2008

---------------------------------------------------------------------

  This file is part of the Microsoft SQL Server Code Samples.
  Copyright (C) Microsoft Corporation.  All rights reserved.

This source code is intended only as a supplement to Microsoft
Development Tools and/or on-line documentation.  See these other
materials for detailed information regarding Microsoft code samples.

THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.

======================================================= */


using System;
using System.Diagnostics;
using System.Collections;
using System.Reflection;
using System.Text;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;


namespace Microsoft.Data.SqlRemoteBlobs.Setup
{
    /// <summary>
    ///     Parses the command line
    /// </summary>
    internal
    sealed
    class CommandLineParser
    {
        /// <summary>
        ///     The callback for the command line parser.
        /// </summary>
        internal
        delegate
        void
        ParserCallback(string[] nextStrings);

        /// <summary>
        ///     Constructor: Command line parser.
        /// </summary>
        internal
        CommandLineParser()
        {
            m_Options = new ArrayList();
            m_ParsedOptions = new ArrayList();
        }

        /// <summary>
        /// Assumes this is the only parser and all commands have been added.
        /// If any command remain or any errors are thrown during parsing 
        /// a list of valid commands from the usage function are returned as
        /// a string.
        /// If no errors are hit the return string is null.
        /// </summary>
        internal
        string
        ParseAll(string[] arguments)
        {
            StringBuilder errorString = new StringBuilder();
            string[] remainingArgs;
            try
            {
                remainingArgs = Parse(arguments);

                // Check all the commands were passed successfully
                //
                bool errorFound = false;
                if (remainingArgs.Length > 0)
                {
                    errorString.Append(InstallProvider.Resources.UnknownArgumentError);
                    foreach (string arg in remainingArgs)
                    {
                        errorString.Append(arg);
                        errorString.Append(" ");
                    }
                    errorString.AppendLine();
                    errorFound = true;
                }

                // Check for any required parameters that were not specified
                //
                foreach (CommandLineOption option in m_Options)
                {
                    if (option.Required && m_ParsedOptions.IndexOf(option) == -1)
                    {
                        errorString.AppendLine(string.Format(CultureInfo.CurrentCulture, 
                            InstallProvider.Resources.RequiredSwitchNotFoundError, option.Name));
                        errorFound = true;
                    }
                }
                if (errorFound)
                {
                   throw new Exception(errorString.ToString());
                }
            }
            catch (Exception ex)
            {
                // Parsing problem. Print out the usage and return error.
                //
                errorString = new StringBuilder();
                errorString.AppendLine(InstallProvider.Resources.ParsingError);
                errorString.AppendLine(ex.Message);
                errorString.AppendLine(Usage());
                return errorString.ToString();
            }
            return null;
        }

        /// <summary>
        /// Outputs a list of valid commands and usage header information.
        /// </summary>
        internal
        string Usage()
        {
            StringBuilder usage = new StringBuilder();
            usage.AppendLine(InstallProvider.Resources.UsageListingError);
            foreach (CommandLineOption option in m_Options)
            {
                usage.AppendFormat(CultureInfo.CurrentCulture,
                    "{0,-23} - {1,-20}\n", option.Name, option.Description);
            }
            return usage.ToString();
        }

        /// <summary>
        ///     This function is used whenever a flag is missing an argument.  It was created
        ///     to implement a uniform look for command line error messages.
        /// </summary>
        /// <param name="argument">
        ///     The name of the argument that wasn't passed.
        /// </param>
        /// <param name="switchName">
        ///     The name of the flag that is missing an argument.
        /// </param>
        [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Needed to make this framework complete.")]
        internal
        static
        void
        MissingArgument(string argument, string switchName)
        {
            throw new Exception(string.Format(CultureInfo.CurrentCulture,
                InstallProvider.Resources.MissingArgumentError, argument, switchName));
        }

        /// <summary>
        ///     Registers a command with the command line parser.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Needed to make this framework complete.")]
        internal
        void
        RegisterCommand(
            string name,
            string description,
            ParserCallback callback
            )
        {
            RegisterCommand(name, description, callback, false);
        }

        /// <summary>
        ///     Registers a command with the command line parser.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Needed to make this framework complete.")]
        internal
        void
        RegisterCommand(
            string name,
            string description,
            ParserCallback callback,
            bool required
            )
        {
           RegisterCommand(name, description, callback, required, 0);
        }

        /// <summary>
        ///     Registers a command with the command line parser.
        /// </summary>
        internal
        void
        RegisterCommand(
            string name,
            string description,
            ParserCallback callback,
            bool required,
            int numberOfExpectedArguments
            )
        {
            RegisterCommand(name, description, callback, required, numberOfExpectedArguments, numberOfExpectedArguments);
        }

        /// <summary>
        ///     Registers a command with the command line parser.
        /// </summary>
        internal
        void
        RegisterCommand(
            string name,
            string description,
            ParserCallback callback,
            bool required,
            int minNumberOfExpectedArguments,
            int maxNumberOfExpectedArguments
            )
        {
            CommandLineOption Option = new CommandLineOption();
            Option.Name = name;
            Option.Description = description;
            Option.Callback = callback;
            Option.Required = required;
            Option.MinNumberOfExpectedArguments = minNumberOfExpectedArguments;
            Option.MaxNumberOfExpectedArguments = maxNumberOfExpectedArguments;
            m_Options.Add(Option);
        }

        /// <summary>
        /// Cycles through the list of commands and sets the matching name to be a required command.
        /// </summary>
        internal
        void
        SetCommandRequired(string name, bool required)
        {
            foreach (CommandLineOption option in m_Options)
            {
                if (string.Equals(name, option.Name, StringComparison.OrdinalIgnoreCase))
                {
                    option.Required = required;
                }
            }
        }

        /// <summary>
        ///     Parse. Takes an array of strings, parses what it can,
        ///     returns the ones it doesn't recognize.
        /// </summary>
        internal
        string[]
        Parse(
            string[] inputStrings
            )
        {
            string[] outputStrings;
            string[] newStrings = inputStrings;
            do {
                m_PushedCommandStrings.Clear();

                // 1. Parse the input we have..
                //
                outputStrings = this.ParseInternal(newStrings);

                // Check for strings which may have been pushed back on to the parse list
                //
                string[] pushedStrings = (string[]) m_PushedCommandStrings.ToArray(typeof(string));

                // Create a new array for the pushed back strings plus the remainder strings
                //
                newStrings = new string[pushedStrings.Length + outputStrings.Length];

                // Copy the two arrays into this new array
                //
                Array.Copy(pushedStrings, 0, newStrings, 0,                    pushedStrings.Length);
                Array.Copy(outputStrings, 0, newStrings, pushedStrings.Length, outputStrings.Length);

            } while(m_PushedCommandStrings.Count != 0);
            return newStrings;
        }

        /// <summary>
        ///     Returns null if this isn't a switch and the switch without the "-" if this is a switch.
        /// </summary>
        internal
        static
        string
        IsSwitch(
            string possibleSwitch
            )
        {
            if (string.IsNullOrEmpty(possibleSwitch)) {
                return null;
            }

            if (possibleSwitch.IndexOf(" ", StringComparison.OrdinalIgnoreCase) != -1)
            {
                // Anything with a space in it is not a switch
                //
                return null;
            }

            if (possibleSwitch.IndexOf("\n", StringComparison.OrdinalIgnoreCase) != -1)
            {
                // Newlines are bad...
                //
                throw new Exception(string.Format(CultureInfo.CurrentCulture,
                    InstallProvider.Resources.NewLineFoundWhileParsingSwitchError, possibleSwitch));
            }

            if (possibleSwitch.IndexOf(c_UnicodeQuoteStart) != -1 ||
                possibleSwitch.IndexOf(c_UnicodeQuoteEnd) != -1)
            {
                // Unicode quotes are bad...
                //
                throw new Exception(string.Format(CultureInfo.CurrentCulture,
                    InstallProvider.Resources.UnicodeQuoteFoundWhileParsingSwitchError, possibleSwitch));
            }

            // If it doesn't start with a switch signifier, it isn't a switch
            //
            bool found = false;
            foreach (char switchSig in c_SwitchSignifiers)
            {
                if (char.Equals(switchSig, possibleSwitch[0])) found = true;
            }
            if (!found)
            {
                return null;
            }

            return possibleSwitch.Remove(0, 1).ToUpper(CultureInfo.InvariantCulture);
        }

        /// <summary>
        ///     Begins parsing the given command line.
        /// </summary>
        private
        string[]
        ParseInternal(string[] CommandStrings)
        {
            // Begin parsing command line arguments. Go through in the order the commands have been added to the
            // command list, not the order they are on the command line. This will make processing of commands
            // the same regardless of order passed on the command line.
            //
            ArrayList commandLine = new ArrayList(CommandStrings.Length);
            commandLine.AddRange(CommandStrings);

            foreach (CommandLineOption currentOption in m_Options) {

                int commandIndex = 0;
                while (commandIndex < commandLine.Count) 
                {
                    string currentCommand = (string) commandLine[commandIndex];
                    string strippedCommand = IsSwitch(currentCommand);

                    // Check to see if this is the current option.
                    // (perform case-insensitive check)
                    //
                    if (strippedCommand == null || 
                        !string.Equals(strippedCommand, currentOption.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        // This doesn't match the current command.
                        //
                        commandIndex += 1;
                    }
                    else 
                    {
                        // Found an option that somebody registered a handler for
                        //
                        commandLine.RemoveAt(commandIndex);

                        // Copy remaining strings into an array. Yes, this is not at all efficient, but
                        // how many times per second do you need to parse the command line?
                        //
                        ArrayList parseList = new ArrayList(commandLine.Count - commandIndex);

                        // This will make sure we get out of the loop at the correct time
                        // without grabbing any additional command line args by accident
                        //
                        int remainingArguments = currentOption.MinNumberOfExpectedArguments;

                        // Keep adding strings to parse list until another
                        // command line switch starting with - or / is found.
                        //
                        while (commandIndex < commandLine.Count &&
                               (remainingArguments > 0 || IsSwitch((string) commandLine[commandIndex]) == null)) 
                        {
                                // Add this to the argument list for this command.
                                // (and remove it from further consideration)
                                //
                                parseList.Add(commandLine[commandIndex]);
                                commandLine.RemoveAt(commandIndex);
                                remainingArguments -= 1;

                        }

                        // Check that the number of arguments provided is as expected.
                        //
                        if (parseList.Count < currentOption.MinNumberOfExpectedArguments)
                        {
                            throw new Exception(string.Format(CultureInfo.CurrentCulture,
                                InstallProvider.Resources.NotEnoughArgumentsSpecifiedError,
                                    currentOption.Name));
                        }

                        if (parseList.Count > currentOption.MaxNumberOfExpectedArguments)
                        {
                            throw new Exception(string.Format(CultureInfo.CurrentCulture,
                                InstallProvider.Resources.ExtraArgumentsSpecifiedError,
                                    currentOption.Name));
                        }

                        currentOption.Callback((string[])parseList.ToArray(typeof(string)));

                        // If an exception wasn't thrown in the callback we've successfully parsed this option
                        //
                        m_ParsedOptions.Add(currentOption);
                    }
                }
            }
            return (string[]) commandLine.ToArray(typeof(string));
        }

        private
        static
        char[]
        c_SwitchSignifiers = {'/','-',(char)8211};

        private
        const
        char
        c_UnicodeQuoteStart = (char)8220;

        private
        const
        char
        c_UnicodeQuoteEnd = (char)8221;

        /// <summary>
        ///     Gets the command line option.
        /// </summary>
        private
        class CommandLineOption
        {
            /// <summary> The name. </summary>
            public string Name;

            /// <summary> The callback. </summary>
            public ParserCallback Callback;

            /// <summary> The minimum number of expected arguments. </summary>
            public int MinNumberOfExpectedArguments;

            /// <summary> The maximum number of expected arguments. </summary>
            public int MaxNumberOfExpectedArguments;

            /// <summary> Required. </summary>
            public bool Required;

            /// <summary> Description. </summary>
            public string Description;
        }

        /// <summary> The current list of strings. </summary>
        private ArrayList m_PushedCommandStrings = new ArrayList();

        /// <summary> The current list of options. </summary>
        private ArrayList m_Options;

        /// <summary> The list of sucessfully parsed options. </summary>
        private ArrayList m_ParsedOptions;
}
};
